#include "stdafx.h"
#include "net/MailMessage.hpp"
#include "net/StringPartSource.hpp"
#include "net/QuotedPrintableEncode.hpp"
#include "stream/StreamCopier.hpp"
#include "stream/stringtokenizer.hpp"
#include "stream/base64encode.hpp"
#include "stream/base64decode.hpp"
using JHCPP::stream::CStreamCopier;
using JHCPP::stream::CStringTokenizer;
using JHCPP::stream::CBase64Encode;
using JHCPP::stream::CBase64Decode;
#include "stdlib/datetimeformat.hpp"
#include "stdlib/datetimeformatter.hpp"
#include "stdlib/datetimeparser.hpp"
#include "stdlib/datetime.hpp"
using JHCPP::stdlib::CDateTime;
using JHCPP::stdlib::CDateTimeFormat;
using JHCPP::stdlib::CDateTimeFormatter;
using JHCPP::stdlib::CDateTimeParser;
#include "utils/numberformatter.hpp"
using JHCPP::utils::CNumberFormatter;


#include <sstream>

NET_NAMESPACE_BEGIN

class CMultiPartHandler: public CPartHandler
{
public:
	CMultiPartHandler(CMailMessage* pMsg) : m_pMsg(pMsg)
	{
	}

	~CMultiPartHandler()
	{
	}

	void handlePart(const CMessageHeader& header, std::istream& stream)
	{
		std::string tmp;
		CStreamCopier::copyToString(stream, tmp);
		if (m_pMsg)
		{
			CMailMessage::ContentTransferEncoding cte = CMailMessage::ENCODING_7BIT;
			if (header.has(CMailMessage::HEADER_CONTENT_TRANSFER_ENCODING))
			{
				std::string enc = header[CMailMessage::HEADER_CONTENT_TRANSFER_ENCODING];
				if (enc == CMailMessage::CTE_8BIT)
					cte = CMailMessage::ENCODING_8BIT;
				else if (enc == CMailMessage::CTE_QUOTED_PRINTABLE)
					cte = CMailMessage::ENCODING_QUOTED_PRINTABLE;
				else if (enc == CMailMessage::CTE_BASE64)
					cte = CMailMessage::ENCODING_BASE64;
			}

			CNameValueCollection::ConstIterator it = header.begin();
			CNameValueCollection::ConstIterator end = header.end();
			CPartSource* pPS = m_pMsg->createPartStore(tmp, 
				header[CMailMessage::HEADER_CONTENT_TYPE], 
				getFileNameFromDisp(it->second));
			jh_check_ptr (pPS);
			for (; it != end; ++it)
			{
				if (CMailMessage::HEADER_CONTENT_DISPOSITION == it->first)
				{
					if (it->second == "inline") m_pMsg->addContent(pPS, cte);
					else m_pMsg->addAttachment("", pPS, cte);
				}

				pPS->headers().set(it->first, it->second);
			}
		}
	}

private:
	std::string getFileNameFromDisp(const std::string& str)
	{
		CStringTokenizer st(str, ";=", CStringTokenizer::TOK_IGNORE_EMPTY | CStringTokenizer::TOK_TRIM);
		CStringTokenizer::Iterator it = st.begin();
		CStringTokenizer::Iterator end = st.end();
		for (; it != end; ++it) { if (*it == "filename") break; }
		if (it != end)
		{
			++it;
			if (it == end) return "";
			return *it;
		}
		return "";
	}

	CMailMessage* m_pMsg;
};


class CStringPartHandler: public CPartHandler
{
public:
	CStringPartHandler(std::string& content) : m_str(content)
	{
	}

	~CStringPartHandler()
	{
	}

	void handlePart(const CMessageHeader& header, std::istream& stream)
	{
		std::string tmp;
		CStreamCopier::copyToString(stream, tmp);
		m_str.append(tmp);
	}

private:
	std::string& m_str;
};


const std::string CMailMessage::HEADER_SUBJECT("Subject");
const std::string CMailMessage::HEADER_FROM("From");
const std::string CMailMessage::HEADER_TO("To");
const std::string CMailMessage::HEADER_CC("CC");
const std::string CMailMessage::HEADER_BCC("BCC");
const std::string CMailMessage::HEADER_DATE("Date");
const std::string CMailMessage::HEADER_CONTENT_TYPE("Content-Type");
const std::string CMailMessage::HEADER_CONTENT_TRANSFER_ENCODING("Content-Transfer-Encoding");
const std::string CMailMessage::HEADER_CONTENT_DISPOSITION("Content-Disposition");
const std::string CMailMessage::HEADER_CONTENT_ID("Content-ID");
const std::string CMailMessage::HEADER_MIME_VERSION("Mime-Version");
const std::string CMailMessage::EMPTY_HEADER;
const std::string CMailMessage::TEXT_PLAIN("text/plain");
const std::string CMailMessage::CTE_7BIT("7bit");
const std::string CMailMessage::CTE_8BIT("8bit");
const std::string CMailMessage::CTE_QUOTED_PRINTABLE("quoted-printable");
const std::string CMailMessage::CTE_BASE64("base64");


CMailMessage::CMailMessage(CPartStoreFactory* pStoreFactory) : m_pStoreFactory(pStoreFactory)
{
	CTimeStamp now;
	setDate(now);
	setContentType("text/plain");
}

CMailMessage::~CMailMessage()
{
	for (PartVec::iterator it = m_parts.begin(); it != m_parts.end(); ++it)
	{
		delete it->pSource;
	}
}

void CMailMessage::addRecipient(const CMailRecipient& recipient)
{
	m_recipients.push_back(recipient);
}

void CMailMessage::setRecipients(const Recipients& recipients)
{
	m_recipients.assign(recipients.begin(), recipients.end());
}

void CMailMessage::setSender(const std::string& sender)
{
	set(HEADER_FROM, sender);
}

const std::string& CMailMessage::getSender() const
{
	if (has(HEADER_FROM))
		return get(HEADER_FROM);
	else
		return EMPTY_HEADER;
}

void CMailMessage::setSubject(const std::string& subject)
{
	set(HEADER_SUBJECT, subject);
}

const std::string& CMailMessage::getSubject() const
{
	if (has(HEADER_SUBJECT))
		return get(HEADER_SUBJECT);
	else
		return EMPTY_HEADER;
}

void CMailMessage::setContent(const std::string& content, ContentTransferEncoding encoding)
{
	m_content  = content;
	m_encoding = encoding;
	set(HEADER_CONTENT_TRANSFER_ENCODING, contentTransferEncodingToString(encoding));
}

void CMailMessage::setContentType(const std::string& mediaType)
{
	set(HEADER_CONTENT_TYPE, mediaType);
}

void CMailMessage::setContentType(const CMediaType& mediaType)
{
	setContentType(mediaType.toString());
}

const std::string& CMailMessage::getContentType() const
{
	if (has(HEADER_CONTENT_TYPE))
		return get(HEADER_CONTENT_TYPE);
	else
		return TEXT_PLAIN;
}

void CMailMessage::setDate(const CTimeStamp& dateTime)
{
	set(HEADER_DATE, CDateTimeFormatter::format(dateTime, CDateTimeFormat::RFC1123_FORMAT));
}

CTimeStamp CMailMessage::getDate() const
{
	const std::string& dateTime = get(HEADER_DATE);
	int tzd;
	return CDateTimeParser::parse(dateTime, tzd).timestamp();
}

bool CMailMessage::isMultipart() const
{
	CMediaType mediaType = getContentType();
	return mediaType.matches("multipart");
}

void CMailMessage::addPart(const std::string& name, CPartSource* pSource, ContentDisposition disposition, ContentTransferEncoding encoding)
{
	jh_check_ptr (pSource);

	makeMultipart();
	Part part;
	part.name        = name;
	part.pSource     = pSource;
	part.disposition = disposition;
	part.encoding    = encoding;
	m_parts.push_back(part);
}

void CMailMessage::addContent(CPartSource* pSource, ContentTransferEncoding encoding)
{
	addPart("", pSource, CONTENT_INLINE, encoding);
}

void CMailMessage::addAttachment(const std::string& name, CPartSource* pSource, ContentTransferEncoding encoding)
{
	addPart(name, pSource, CONTENT_ATTACHMENT, encoding);
}

void CMailMessage::read(std::istream& istr, CPartHandler& handler)
{
	readHeader(istr);
	if (isMultipart())
	{
		readMultipart(istr, handler);
	}
	else
	{
		CStringPartHandler handler(m_content);
		readPart(istr, *this, handler);
	}
}

void CMailMessage::read(std::istream& istr)
{
	readHeader(istr);
	if (isMultipart())
	{
		CMultiPartHandler handler(this);
		readMultipart(istr, handler);
	}
	else
	{
		CStringPartHandler handler(m_content);
		readPart(istr, *this, handler);
	}
}

void CMailMessage::write(std::ostream& ostr) const
{
	CMessageHeader header(*this);
	setRecipientHeaders(header);
	if (isMultipart())
	{
		writeMultipart(header, ostr);
	}
	else
	{
		writeHeader(header, ostr);
		std::istringstream istr(m_content);
		writeEncoded(istr, ostr, m_encoding);
	}
}

void CMailMessage::makeMultipart()
{
	if (!isMultipart())
	{
		CMediaType mediaType("multipart", "mixed");
		setContentType(mediaType);	
	}
}

void CMailMessage::writeHeader(const CMessageHeader& header, std::ostream& ostr) const
{
	header.write(ostr);
	ostr << "\r\n";
}

void CMailMessage::writeMultipart(CMessageHeader& header, std::ostream& ostr) const
{
	if (m_boundary.empty()) m_boundary = CMultipartWriter::createBoundary();
	CMediaType mediaType(getContentType());
	mediaType.setParameter("boundary", m_boundary);
	header.set(HEADER_CONTENT_TYPE, mediaType.toString());
	header.set(HEADER_MIME_VERSION, "1.0");
	writeHeader(header, ostr);

	CMultipartWriter writer(ostr, m_boundary);
	for (PartVec::const_iterator it = m_parts.begin(); it != m_parts.end(); ++it)
	{
		writePart(writer, *it);
	}
	writer.close();
}

void CMailMessage::writePart(CMultipartWriter& writer, const Part& part) const
{
	CMessageHeader partHeader(part.pSource->headers());
	CMediaType mediaType(part.pSource->mediaType());
	if (!part.name.empty())
		mediaType.setParameter("name", part.name);
	partHeader.set(HEADER_CONTENT_TYPE, mediaType.toString());
	partHeader.set(HEADER_CONTENT_TRANSFER_ENCODING, contentTransferEncodingToString(part.encoding));
	std::string disposition;
	if (part.disposition == CONTENT_ATTACHMENT)
	{
		disposition = "attachment";
		const std::string& filename = part.pSource->filename();
		if (!filename.empty())
		{
			disposition.append("; filename=");
			quote(filename, disposition);
		}
	}
	else disposition = "inline";
	partHeader.set(HEADER_CONTENT_DISPOSITION, disposition);
	writer.nextPart(partHeader);
	writeEncoded(part.pSource->stream(), writer.stream(), part.encoding);
}

void CMailMessage::writeEncoded(std::istream& istr, std::ostream& ostr, ContentTransferEncoding encoding) const
{
	switch (encoding)
	{
	case ENCODING_7BIT:
	case ENCODING_8BIT:
		CStreamCopier::copyStream(istr, ostr);
		break;
	case ENCODING_QUOTED_PRINTABLE:
		{
			CQuotedPrintableEncode encoder(ostr);
			CStreamCopier::copyStream(istr, encoder);
			encoder.close();
		}
		break;
	case ENCODING_BASE64:
		{
			CBase64Encode encoder(ostr);
			CStreamCopier::copyStream(istr, encoder);
			encoder.close();
		}
		break;
	}
}

void CMailMessage::readHeader(std::istream& istr)
{
	clear();
	CMessageHeader::read(istr);
	istr.get(); // \r
	if ('\n' == istr.peek()) istr.get(); // \n
}

void CMailMessage::readMultipart(std::istream& istr, CPartHandler& handler)
{
	CMediaType contentType(getContentType());
	m_boundary = contentType.getParameter("boundary");
	CMultipartReader reader(istr, m_boundary);
	while (reader.hasNextPart())
	{
		CMessageHeader partHeader;
		reader.nextPart(partHeader);
		readPart(reader.stream(), partHeader, handler);
	}
}

void CMailMessage::readPart(std::istream& istr, const CMessageHeader& header, CPartHandler& handler)
{
	std::string encoding;
	if (header.has(HEADER_CONTENT_TRANSFER_ENCODING))
	{
		encoding = header.get(HEADER_CONTENT_TRANSFER_ENCODING);
		// get rid of a parameter if one is set
		std::string::size_type pos = encoding.find(';');
		if (pos != std::string::npos)
			encoding.resize(pos);
	}
	if (icompare(encoding, CTE_QUOTED_PRINTABLE) == 0)
	{
		CQuotedPrintableDecode decoder(istr);
		handlePart(decoder, header, handler);
	}
	else if (icompare(encoding, CTE_BASE64) == 0)
	{
		CBase64Decode decoder(istr);
		handlePart(decoder, header, handler);
	}
	else
	{
		handlePart(istr, header, handler);
	}
}

void CMailMessage::handlePart(std::istream& istr, const CMessageHeader& header, CPartHandler& handler)
{
	handler.handlePart(header, istr);
	// Read remaining characters from stream in case
	// the handler failed to read the complete stream.
	while (istr.good()) istr.get();
}

void CMailMessage::setRecipientHeaders(CMessageHeader& headers) const
{
	std::string to;
	std::string cc;
	std::string bcc;

	for (Recipients::const_iterator it = m_recipients.begin(); it != m_recipients.end(); ++it)
	{
		switch (it->getType())
		{
		case CMailRecipient::PRIMARY_RECIPIENT:
			appendRecipient(*it, to);
			break;
		case CMailRecipient::CC_RECIPIENT:
			appendRecipient(*it, cc);
			break;
		case CMailRecipient::BCC_RECIPIENT:
			break;
		}
	}
	if (!to.empty()) headers.set(HEADER_TO, to);
	if (!cc.empty()) headers.set(HEADER_CC, cc);
}

const std::string& CMailMessage::contentTransferEncodingToString(ContentTransferEncoding encoding)
{
	switch (encoding)
	{
	case ENCODING_7BIT:
		return CTE_7BIT;
	case ENCODING_8BIT:
		return CTE_8BIT;
	case ENCODING_QUOTED_PRINTABLE:
		return CTE_QUOTED_PRINTABLE;
	case ENCODING_BASE64:
		return CTE_BASE64;
	default:
		jh_bugcheck();
	}
	return CTE_7BIT;
}

int CMailMessage::lineLength(const std::string& str)
{
	int n = 0;
	std::string::const_reverse_iterator it  = str.rbegin();
	std::string::const_reverse_iterator end = str.rend();
	while (it != end && *it != '\n') { ++n; ++it; }
	return n;
}

void CMailMessage::appendRecipient(const CMailRecipient& recipient, std::string& str)
{
	if (!str.empty()) str.append(", ");
	const std::string& realName = recipient.getRealName();
	const std::string& address  = recipient.getAddress();
	std::string rec;
	if (!realName.empty())
	{
		quote(realName, rec, true);
		rec.append(" ");
	}
	rec.append("<");
	rec.append(address);
	rec.append(">");
	if (lineLength(str) + rec.length() > 70) str.append("\r\n\t");
	str.append(rec);
}

std::string CMailMessage::encodeWord(const std::string& text, const std::string& charset)
{
	bool containsNonASCII = false;
	for (std::string::const_iterator it = text.begin(); it != text.end(); ++it)
	{
		if (static_cast<unsigned char>(*it) > 127)
		{
			containsNonASCII = true;
			break;
		}
	}
	if (!containsNonASCII) return text;

	std::string encodedText;
	std::string::size_type lineLength = 0;
	for (std::string::const_iterator it = text.begin(); it != text.end(); ++it)
	{
		if (lineLength == 0)
		{
			encodedText += "=?";
			encodedText += charset;
			encodedText += "?q?";
			lineLength += charset.length() + 5;
		}
		switch (*it)
		{
		case ' ':
			encodedText += '_';
			lineLength++;
			break;
		case '=':
		case '?':
		case '_':
		case '(':
		case ')':
		case '[':
		case ']':
		case '<':
		case '>':
		case ',':
		case ';':
		case ':':
		case '.':
		case '@':
			encodedText += '=';
			CNumberFormatter::appendHex(encodedText, static_cast<unsigned>(static_cast<unsigned char>(*it)), 2);
			lineLength += 3;
			break;
		default:
			if (*it > 32 && *it < 127)
			{
				encodedText += *it;
				lineLength++;
			}
			else
			{
				encodedText += '=';
				CNumberFormatter::appendHex(encodedText, static_cast<unsigned>(static_cast<unsigned char>(*it)), 2);
				lineLength += 3;
			}
		}
		if ((lineLength >= 64 && (*it == ' ' || *it == '\t' || *it == '\r' || *it == '\n')) || lineLength >= 72)
		{
			encodedText += "?=\r\n ";
			lineLength = 0;
		}
	}
	if (lineLength > 0)
	{
		encodedText += "?=";
	}	
	return encodedText;
}

CPartSource* CMailMessage::createPartStore(const std::string& content, const std::string& mediaType, const std::string& filename)
{
	if (!m_pStoreFactory) return new CStringPartSource(content, mediaType, filename);
	else return m_pStoreFactory->createPartStore(content, mediaType, filename);
}

const CMailMessage::Recipients& CMailMessage::recipients() const
{
	return m_recipients;
}

const std::string& CMailMessage::getContent() const
{
	return m_content;
}

const CMailMessage::PartVec& CMailMessage::parts() const
{
	return m_parts;
}


NET_NAMESPACE_END
